import { BoundingSphere } from '../../Source/Cesium.js';
import { Cartesian3 } from '../../Source/Cesium.js';
import { Cartographic } from '../../Source/Cesium.js';
import { Ellipsoid } from '../../Source/Cesium.js';
import { EncodedCartesian3 } from '../../Source/Cesium.js';
import { GeographicProjection } from '../../Source/Cesium.js';
import { Intersect } from '../../Source/Cesium.js';
import { Interval } from '../../Source/Cesium.js';
import { Math as CesiumMath } from '../../Source/Cesium.js';
import { Matrix4 } from '../../Source/Cesium.js';
import { OrientedBoundingBox } from '../../Source/Cesium.js';
import { Plane } from '../../Source/Cesium.js';
import { Rectangle } from '../../Source/Cesium.js';
import createPackableSpecs from '../createPackableSpecs.js';

describe('Core/BoundingSphere', function() {

    var positionsRadius = 1.0;
    var positionsCenter = new Cartesian3(10000001.0, 0.0, 0.0);

    var center = new Cartesian3(10000000.0, 0.0, 0.0);

    function getPositions() {
        return [
                Cartesian3.add(center, new Cartesian3(1, 0, 0), new Cartesian3()),
                Cartesian3.add(center, new Cartesian3(2, 0, 0), new Cartesian3()),
                Cartesian3.add(center, new Cartesian3(0, 0, 0), new Cartesian3()),
                Cartesian3.add(center, new Cartesian3(1, 1, 0), new Cartesian3()),
                Cartesian3.add(center, new Cartesian3(1, -1, 0), new Cartesian3()),
                Cartesian3.add(center, new Cartesian3(1, 0, 1), new Cartesian3()),
                Cartesian3.add(center, new Cartesian3(1, 0, -1), new Cartesian3())
            ];
    }

    function getPositionsAsFlatArray() {
        var positions = getPositions();
        var result = [];
        for (var i = 0; i < positions.length; ++i) {
            result.push(positions[i].x);
            result.push(positions[i].y);
            result.push(positions[i].z);
        }
        return result;
    }

    function getPositionsAsFlatArrayWithStride5() {
        var positions = getPositions();
        var result = [];
        for (var i = 0; i < positions.length; ++i) {
            result.push(positions[i].x);
            result.push(positions[i].y);
            result.push(positions[i].z);
            result.push(1.23);
            result.push(4.56);
        }
        return result;
    }

    function getPositionsAsEncodedFlatArray() {
        var positions = getPositions();
        var high = [];
        var low = [];
        for (var i = 0; i < positions.length; ++i) {
            var encoded = EncodedCartesian3.fromCartesian(positions[i]);
            high.push(encoded.high.x);
            high.push(encoded.high.y);
            high.push(encoded.high.z);
            low.push(encoded.low.x);
            low.push(encoded.low.y);
            low.push(encoded.low.z);
        }
        return {
            high : high,
            low : low
        };
    }

    it('default constructing produces expected values', function() {
        var sphere = new BoundingSphere();
        expect(sphere.center).toEqual(Cartesian3.ZERO);
        expect(sphere.radius).toEqual(0.0);
    });

    it('constructor sets expected values', function() {
        var expectedCenter = new Cartesian3(1.0, 2.0, 3.0);
        var expectedRadius = 1.0;
        var sphere = new BoundingSphere(expectedCenter, expectedRadius);
        expect(sphere.center).toEqual(expectedCenter);
        expect(sphere.radius).toEqual(expectedRadius);
    });

    it('clone without a result parameter', function() {
        var sphere = new BoundingSphere(new Cartesian3(1.0, 2.0, 3.0), 4.0);
        var result = sphere.clone();
        expect(sphere).not.toBe(result);
        expect(sphere).toEqual(result);
    });

    it('clone with a result parameter', function() {
        var sphere = new BoundingSphere(new Cartesian3(1.0, 2.0, 3.0), 4.0);
        var result = new BoundingSphere();
        var returnedResult = sphere.clone(result);
        expect(result).not.toBe(sphere);
        expect(result).toBe(returnedResult);
        expect(result).toEqual(sphere);
    });

    it('clone works with "this" result parameter', function() {
        var expectedCenter = new Cartesian3(1.0, 2.0, 3.0);
        var expectedRadius = 1.0;
        var sphere = new BoundingSphere(expectedCenter, expectedRadius);
        var returnedResult = sphere.clone(sphere);
        expect(sphere).toBe(returnedResult);
        expect(sphere.center).toEqual(expectedCenter);
        expect(sphere.radius).toEqual(expectedRadius);
    });

    it('clone clones undefined', function() {
        expect(BoundingSphere.clone(undefined)).toBe(undefined);
    });

    it('equals', function() {
        var sphere = new BoundingSphere(new Cartesian3(1.0, 2.0, 3.0), 4.0);
        expect(sphere.equals(new BoundingSphere(new Cartesian3(1.0, 2.0, 3.0), 4.0))).toEqual(true);
        expect(sphere.equals(new BoundingSphere(new Cartesian3(5.0, 2.0, 3.0), 4.0))).toEqual(false);
        expect(sphere.equals(new BoundingSphere(new Cartesian3(1.0, 6.0, 3.0), 4.0))).toEqual(false);
        expect(sphere.equals(new BoundingSphere(new Cartesian3(1.0, 2.0, 7.0), 4.0))).toEqual(false);
        expect(sphere.equals(new BoundingSphere(new Cartesian3(1.0, 2.0, 3.0), 8.0))).toEqual(false);
        expect(sphere.equals(undefined)).toEqual(false);
    });

    it('fromPoints without positions returns an empty sphere', function() {
        var sphere = BoundingSphere.fromPoints();
        expect(sphere.center).toEqual(Cartesian3.ZERO);
        expect(sphere.radius).toEqual(0.0);
    });

    it('fromPoints works with one point', function() {
        var expectedCenter = new Cartesian3(1.0, 2.0, 3.0);
        var sphere = BoundingSphere.fromPoints([expectedCenter]);
        expect(sphere.center).toEqual(expectedCenter);
        expect(sphere.radius).toEqual(0.0);
    });

    it('fromPoints computes a center from points', function() {
        var sphere = BoundingSphere.fromPoints(getPositions());
        expect(sphere.center).toEqual(positionsCenter);
        expect(sphere.radius).toEqual(positionsRadius);
    });

    it('fromPoints contains all points (naive)', function() {
        var sphere = BoundingSphere.fromPoints(getPositions());
        var radius = sphere.radius;
        var center = sphere.center;

        var r = new Cartesian3(radius, radius, radius);
        var max = Cartesian3.add(r, center, new Cartesian3());
        var min = Cartesian3.subtract(center, r, new Cartesian3());

        var positions = getPositions();
        var numPositions = positions.length;
        for ( var i = 0; i < numPositions; i++) {
            var currentPos = positions[i];
            expect(currentPos.x <= max.x && currentPos.x >= min.x).toEqual(true);
            expect(currentPos.y <= max.y && currentPos.y >= min.y).toEqual(true);
            expect(currentPos.z <= max.z && currentPos.z >= min.z).toEqual(true);
        }
    });

    it('fromPoints contains all points (ritter)', function() {
        var positions = getPositions();
        positions.push(new Cartesian3(1, 1, 1), new Cartesian3(2, 2, 2), new Cartesian3(3, 3, 3));
        var sphere = BoundingSphere.fromPoints(positions);
        var radius = sphere.radius;
        var center = sphere.center;

        var r = new Cartesian3(radius, radius, radius);
        var max = Cartesian3.add(r, center, new Cartesian3());
        var min = Cartesian3.subtract(center, r, new Cartesian3());

        var numPositions = positions.length;
        for ( var i = 0; i < numPositions; i++) {
            var currentPos = positions[i];
            expect(currentPos.x <= max.x && currentPos.x >= min.x).toEqual(true);
            expect(currentPos.y <= max.y && currentPos.y >= min.y).toEqual(true);
            expect(currentPos.z <= max.z && currentPos.z >= min.z).toEqual(true);
        }
    });

    it('fromVertices without positions returns an empty sphere', function() {
        var sphere = BoundingSphere.fromVertices();
        expect(sphere.center).toEqual(Cartesian3.ZERO);
        expect(sphere.radius).toEqual(0.0);
    });

    it('fromVertices works with one point', function() {
        var expectedCenter = new Cartesian3(1.0, 2.0, 3.0);
        var sphere = BoundingSphere.fromVertices([expectedCenter.x, expectedCenter.y, expectedCenter.z]);
        expect(sphere.center).toEqual(expectedCenter);
        expect(sphere.radius).toEqual(0.0);
    });

    it('fromVertices computes a center from points', function() {
        var sphere = BoundingSphere.fromVertices(getPositionsAsFlatArray());
        expect(sphere.center).toEqual(positionsCenter);
        expect(sphere.radius).toEqual(positionsRadius);
    });

    it('fromVertices contains all points (naive)', function() {
        var sphere = BoundingSphere.fromVertices(getPositionsAsFlatArray());
        var radius = sphere.radius;
        var center = sphere.center;

        var r = new Cartesian3(radius, radius, radius);
        var max = Cartesian3.add(r, center, new Cartesian3());
        var min = Cartesian3.subtract(center, r, new Cartesian3());

        var positions = getPositions();
        var numPositions = positions.length;
        for ( var i = 0; i < numPositions; i++) {
            var currentPos = positions[i];
            expect(currentPos.x <= max.x && currentPos.x >= min.x).toEqual(true);
            expect(currentPos.y <= max.y && currentPos.y >= min.y).toEqual(true);
            expect(currentPos.z <= max.z && currentPos.z >= min.z).toEqual(true);
        }
    });

    it('fromVertices contains all points (ritter)', function() {
        var positions = getPositionsAsFlatArray();
        positions.push(1, 1, 1,  2, 2, 2,  3, 3, 3);
        var sphere = BoundingSphere.fromVertices(positions);
        var radius = sphere.radius;
        var center = sphere.center;

        var r = new Cartesian3(radius, radius, radius);
        var max = Cartesian3.add(r, center, new Cartesian3());
        var min = Cartesian3.subtract(center, r, new Cartesian3());

        var numElements = positions.length;
        for (var i = 0; i < numElements; i += 3) {
            expect(positions[i] <= max.x && positions[i] >= min.x).toEqual(true);
            expect(positions[i + 1] <= max.y && positions[i + 1] >= min.y).toEqual(true);
            expect(positions[i + 2] <= max.z && positions[i + 2] >= min.z).toEqual(true);
        }
    });

    it('fromVertices works with a stride of 5', function() {
        var sphere = BoundingSphere.fromVertices(getPositionsAsFlatArrayWithStride5(), undefined, 5);
        expect(sphere.center).toEqual(positionsCenter);
        expect(sphere.radius).toEqual(positionsRadius);
    });

    it('fromVertices works with defined center', function() {
        var center = new Cartesian3(1.0, 2.0, 3.0);
        var sphere = BoundingSphere.fromVertices(getPositionsAsFlatArrayWithStride5(), center, 5);
        expect(sphere.center).toEqual(Cartesian3.add(positionsCenter, center, new Cartesian3()));
        expect(sphere.radius).toEqual(positionsRadius);
    });

    it('fromVertices requires a stride of at least 3', function() {
        function callWithStrideOf2() {
            BoundingSphere.fromVertices(getPositionsAsFlatArray(), undefined, 2);
        }
        expect(callWithStrideOf2).toThrowDeveloperError();
    });

    it('fromVertices fills result parameter if specified', function() {
        var center = new Cartesian3(1.0, 2.0, 3.0);
        var result = new BoundingSphere();
        var sphere = BoundingSphere.fromVertices(getPositionsAsFlatArrayWithStride5(), center, 5, result);
        expect(sphere).toEqual(result);
        expect(result.center).toEqual(Cartesian3.add(positionsCenter, center, new Cartesian3()));
        expect(result.radius).toEqual(positionsRadius);
    });

    it('fromEncodedCartesianVertices without positions returns an empty sphere', function() {
        var sphere = BoundingSphere.fromEncodedCartesianVertices();
        expect(sphere.center).toEqual(Cartesian3.ZERO);
        expect(sphere.radius).toEqual(0.0);
    });

    it('fromEncodedCartesianVertices without positions of different lengths returns an empty sphere', function() {
        var positions = getPositionsAsEncodedFlatArray();
        positions.low.length = positions.low.length - 1;
        var sphere = BoundingSphere.fromEncodedCartesianVertices(positions.high, positions.low);
        expect(sphere.center).toEqual(Cartesian3.ZERO);
        expect(sphere.radius).toEqual(0.0);
    });

    it('fromEncodedCartesianVertices computes a center from points', function() {
        var positions = getPositionsAsEncodedFlatArray();
        var sphere = BoundingSphere.fromEncodedCartesianVertices(positions.high, positions.low);
        expect(sphere.center).toEqual(positionsCenter);
        expect(sphere.radius).toEqual(positionsRadius);
    });

    it('fromEncodedCartesianVertices contains all points (naive)', function() {
        var positions = getPositionsAsEncodedFlatArray();
        var sphere = BoundingSphere.fromEncodedCartesianVertices(positions.high, positions.low);
        var radius = sphere.radius;
        var center = sphere.center;

        var r = new Cartesian3(radius, radius, radius);
        var max = Cartesian3.add(r, center, new Cartesian3());
        var min = Cartesian3.subtract(center, r, new Cartesian3());

        positions = getPositions();
        var numPositions = positions.length;
        for ( var i = 0; i < numPositions; i++) {
            var currentPos = positions[i];
            expect(currentPos.x <= max.x && currentPos.x >= min.x).toEqual(true);
            expect(currentPos.y <= max.y && currentPos.y >= min.y).toEqual(true);
            expect(currentPos.z <= max.z && currentPos.z >= min.z).toEqual(true);
        }
    });

    it('fromEncodedCartesianVertices contains all points (ritter)', function() {
        var positions = getPositionsAsEncodedFlatArray();
        var appendedPositions = [new Cartesian3(1, 1, 1), new Cartesian3(2, 2, 2), new Cartesian3(3, 3, 3)];
        for (var j = 0; j < appendedPositions.length; ++j) {
            var encoded = EncodedCartesian3.fromCartesian(Cartesian3.add(appendedPositions[j], center, new Cartesian3()));
            positions.high.push(encoded.high.x);
            positions.high.push(encoded.high.y);
            positions.high.push(encoded.high.z);
            positions.low.push(encoded.low.x);
            positions.low.push(encoded.low.y);
            positions.low.push(encoded.low.z);
        }

        var sphere = BoundingSphere.fromEncodedCartesianVertices(positions.high, positions.low);
        var radius = sphere.radius;
        var sphereCenter = sphere.center;

        var r = new Cartesian3(radius, radius, radius);
        var max = Cartesian3.add(r, sphereCenter, new Cartesian3());
        var min = Cartesian3.subtract(sphereCenter, r, new Cartesian3());

        var numElements = positions.length;
        for (var i = 0; i < numElements; i += 3) {
            expect(positions[i] <= max.x && positions[i] >= min.x).toEqual(true);
            expect(positions[i + 1] <= max.y && positions[i + 1] >= min.y).toEqual(true);
            expect(positions[i + 2] <= max.z && positions[i + 2] >= min.z).toEqual(true);
        }
    });

    it('fromEncodedCartesianVertices fills result parameter if specified', function() {
        var positions = getPositionsAsEncodedFlatArray();
        var result = new BoundingSphere();
        var sphere = BoundingSphere.fromEncodedCartesianVertices(positions.high, positions.low, result);
        expect(sphere).toEqual(result);
        expect(result.center).toEqual(positionsCenter);
        expect(result.radius).toEqual(positionsRadius);
    });

    it('fromRectangle2D creates an empty sphere if no rectangle provided', function() {
        var sphere = BoundingSphere.fromRectangle2D();
        expect(sphere.center).toEqual(Cartesian3.ZERO);
        expect(sphere.radius).toEqual(0.0);
    });

    it('fromRectangle2D', function() {
        var rectangle = Rectangle.MAX_VALUE;
        var projection = new GeographicProjection(Ellipsoid.UNIT_SPHERE);
        var expected = new BoundingSphere(Cartesian3.ZERO, Math.sqrt(rectangle.east * rectangle.east + rectangle.north * rectangle.north));
        expect(BoundingSphere.fromRectangle2D(rectangle, projection)).toEqual(expected);
    });

    it('fromRectangle3D creates an empty sphere if no rectangle provided', function() {
        var sphere = BoundingSphere.fromRectangle3D();
        expect(sphere.center).toEqual(Cartesian3.ZERO);
        expect(sphere.radius).toEqual(0.0);
    });

    it('fromRectangle3D', function() {
        var rectangle = Rectangle.MAX_VALUE;
        var ellipsoid = Ellipsoid.WGS84;
        var expected = new BoundingSphere(Cartesian3.ZERO, ellipsoid.maximumRadius);
        expect(BoundingSphere.fromRectangle3D(rectangle, ellipsoid)).toEqual(expected);
    });

    it('fromRectangle3D with height', function() {
        var rectangle = new Rectangle(0.1, -0.3, 0.2, -0.4);
        var height = 100000.0;
        var ellipsoid = Ellipsoid.WGS84;
        var points = Rectangle.subsample(rectangle, ellipsoid, height);
        var expected = BoundingSphere.fromPoints(points);
        expect(BoundingSphere.fromRectangle3D(rectangle, ellipsoid, height)).toEqual(expected);
    });

    it('fromCornerPoints', function() {
        var sphere = BoundingSphere.fromCornerPoints(new Cartesian3(-1.0, -0.0, 0.0), new Cartesian3(1.0, 0.0, 0.0));
        expect(sphere).toEqual(new BoundingSphere(Cartesian3.ZERO, 1.0));
    });

    it('fromCornerPoints with a result parameter', function() {
        var sphere = new BoundingSphere();
        var result = BoundingSphere.fromCornerPoints(new Cartesian3(0.0, -1.0, 0.0), new Cartesian3(0.0, 1.0, 0.0), sphere);
        expect(result).toBe(sphere);
        expect(result).toEqual(new BoundingSphere(Cartesian3.ZERO, 1.0));
    });

    it('fromCornerPoints throws without corner', function() {
        expect(function() {
            BoundingSphere.fromCornerPoints();
        }).toThrowDeveloperError();
    });

    it('fromCornerPoints throws without oppositeCorner', function() {
        expect(function() {
            BoundingSphere.fromCornerPoints(Cartesian3.UNIT_X);
        }).toThrowDeveloperError();
    });

    it('fromEllipsoid', function() {
        var ellipsoid = Ellipsoid.WGS84;
        var sphere = BoundingSphere.fromEllipsoid(ellipsoid);
        expect(sphere.center).toEqual(Cartesian3.ZERO);
        expect(sphere.radius).toEqual(ellipsoid.maximumRadius);
    });

    it('fromEllipsoid with a result parameter', function() {
        var ellipsoid = Ellipsoid.WGS84;
        var sphere = new BoundingSphere(new Cartesian3(1.0, 2.0, 3.0), 4.0);
        var result = BoundingSphere.fromEllipsoid(ellipsoid, sphere);
        expect(result).toBe(sphere);
        expect(result).toEqual(new BoundingSphere(Cartesian3.ZERO, ellipsoid.maximumRadius));
    });

    it('fromEllipsoid throws without ellipsoid', function() {
        expect(function() {
            BoundingSphere.fromEllipsoid();
        }).toThrowDeveloperError();
    });

    it('fromBoundingSpheres with undefined returns an empty sphere', function() {
        var sphere = BoundingSphere.fromBoundingSpheres();
        expect(sphere.center).toEqual(Cartesian3.ZERO);
        expect(sphere.radius).toEqual(0.0);
    });

    it('fromBoundingSpheres with empty array returns an empty sphere', function() {
        var sphere = BoundingSphere.fromBoundingSpheres([]);
        expect(sphere.center).toEqual(Cartesian3.ZERO);
        expect(sphere.radius).toEqual(0.0);
    });

    it('fromBoundingSpheres works with 1 sphere', function() {
        var one = new BoundingSphere(new Cartesian3(1, 2, 3), 4);

        var sphere = BoundingSphere.fromBoundingSpheres([one]);
        expect(sphere).toEqual(one);
    });

    it('fromBoundingSpheres works with 2 spheres', function() {
        var one = new BoundingSphere(new Cartesian3(1, 2, 3), 4);
        var two = new BoundingSphere(new Cartesian3(5, 6, 7), 8);

        var sphere = BoundingSphere.fromBoundingSpheres([one, two]);
        expect(sphere).toEqual(BoundingSphere.union(one, two, new BoundingSphere()));
    });

    it('fromBoundingSpheres works with 3 spheres', function() {
        var one = new BoundingSphere(new Cartesian3(0, 0, 0), 1);
        var two = new BoundingSphere(new Cartesian3(0, 3, 0), 1);
        var three = new BoundingSphere(new Cartesian3(0, 0, 4), 1);

        var expected = new BoundingSphere(new Cartesian3(0.0, 1.5, 2.0), 3.5);
        var sphere = BoundingSphere.fromBoundingSpheres([one, two, three]);
        expect(sphere).toEqual(expected);
    });

    it('fromOrientedBoundingBox works with a result', function() {
        var box = OrientedBoundingBox.fromPoints(getPositions());
        var sphere = new BoundingSphere();
        BoundingSphere.fromOrientedBoundingBox(box, sphere);
        expect(sphere.center).toEqual(positionsCenter);
        expect(sphere.radius).toBeGreaterThan(1.5);
        expect(sphere.radius).toBeLessThan(2.0);
    });

    it('fromOrientedBoundingBox works without a result parameter', function() {
        var box = OrientedBoundingBox.fromPoints(getPositions());
        var sphere = BoundingSphere.fromOrientedBoundingBox(box);
        expect(sphere.center).toEqual(positionsCenter);
        expect(sphere.radius).toBeGreaterThan(1.5);
        expect(sphere.radius).toBeLessThan(2.0);
    });

    it('throws from fromOrientedBoundingBox with undefined orientedBoundingBox parameter', function() {
        expect(function() {
            BoundingSphere.fromOrientedBoundingBox(undefined);
        }).toThrowDeveloperError();
    });

    it('intersectPlane with sphere on the positive side of a plane', function() {
        var sphere = new BoundingSphere(Cartesian3.ZERO, 0.5);
        var normal = Cartesian3.negate(Cartesian3.UNIT_X, new Cartesian3());
        var position = Cartesian3.UNIT_X;
        var plane = new Plane(normal, -Cartesian3.dot(normal, position));
        expect(sphere.intersectPlane(plane)).toEqual(Intersect.INSIDE);
    });

    it('intersectPlane with sphere on the negative side of a plane', function() {
        var sphere = new BoundingSphere(Cartesian3.ZERO, 0.5);
        var normal = Cartesian3.UNIT_X;
        var position = Cartesian3.UNIT_X;
        var plane = new Plane(normal, -Cartesian3.dot(normal, position));
        expect(sphere.intersectPlane(plane)).toEqual(Intersect.OUTSIDE);
    });

    it('intersectPlane with sphere intersecting a plane', function() {
        var sphere = new BoundingSphere(Cartesian3.UNIT_X, 0.5);
        var normal = Cartesian3.UNIT_X;
        var position = Cartesian3.UNIT_X;
        var plane = new Plane(normal, -Cartesian3.dot(normal, position));
        expect(sphere.intersectPlane(plane)).toEqual(Intersect.INTERSECTING);
    });

    it('expands to contain another sphere', function() {
        var bs1 = new BoundingSphere(Cartesian3.negate(Cartesian3.UNIT_X, new Cartesian3()), 1.0);
        var bs2 = new BoundingSphere(Cartesian3.UNIT_X, 1.0);
        var expected = new BoundingSphere(Cartesian3.ZERO, 2.0);
        expect(BoundingSphere.union(bs1, bs2)).toEqual(expected);
    });

    it('union left sphere encloses right', function() {
        var bs1 = new BoundingSphere(Cartesian3.ZERO, 3.0);
        var bs2 = new BoundingSphere(Cartesian3.UNIT_X, 1.0);
        var union = BoundingSphere.union(bs1, bs2);
        expect(union).toEqual(bs1);
    });

    it('union of co-located spheres, right sphere encloses left', function() {
        var bs1 = new BoundingSphere(Cartesian3.UNIT_X, 1.0);
        var bs2 = new BoundingSphere(Cartesian3.UNIT_X, 2.0);
        var union = BoundingSphere.union(bs1, bs2);
        expect(union).toEqual(bs2);
    });

    it('union result parameter is a tight fit', function() {
        var bs1 = new BoundingSphere(Cartesian3.multiplyByScalar(Cartesian3.negate(Cartesian3.UNIT_X, new Cartesian3()), 3.0, new Cartesian3()), 3.0);
        var bs2 = new BoundingSphere(Cartesian3.UNIT_X, 1.0);
        var expected = new BoundingSphere(Cartesian3.multiplyByScalar(Cartesian3.negate(Cartesian3.UNIT_X, new Cartesian3()), 2.0, new Cartesian3()), 4.0);
        BoundingSphere.union(bs1, bs2, bs1);
        expect(bs1).toEqual(expected);
    });

    it('expands to contain another point', function() {
        var bs = new BoundingSphere(Cartesian3.negate(Cartesian3.UNIT_X, new Cartesian3()), 1.0);
        var point = Cartesian3.UNIT_X;
        var expected = new BoundingSphere(Cartesian3.negate(Cartesian3.UNIT_X, new Cartesian3()), 2.0);
        expect(BoundingSphere.expand(bs, point)).toEqual(expected);
    });

    it('applies transform', function() {
        var bs = new BoundingSphere(Cartesian3.ZERO, 1.0);
        var transform = Matrix4.fromTranslation(new Cartesian3(1.0, 2.0, 3.0));
        var expected = new BoundingSphere(new Cartesian3(1.0, 2.0, 3.0), 1.0);
        expect(BoundingSphere.transform(bs, transform)).toEqual(expected);
    });

    it('applies scale transform', function() {
        var bs = new BoundingSphere(Cartesian3.ZERO, 1.0);
        var transform = Matrix4.fromScale(new Cartesian3(1.0, 2.0, 3.0));
        var expected = new BoundingSphere(Cartesian3.ZERO, 3.0);
        expect(BoundingSphere.transform(bs, transform)).toEqual(expected);
    });

    it('applies transform without scale', function() {
        var bs = new BoundingSphere(Cartesian3.ZERO, 1.0);
        var transform = Matrix4.fromTranslation(new Cartesian3(1.0, 2.0, 3.0));
        var expected = new BoundingSphere(new Cartesian3(1.0, 2.0, 3.0), 1.0);
        expect(BoundingSphere.transformWithoutScale(bs, transform)).toEqual(expected);
    });

    it('transformWithoutScale ignores scale', function() {
        var bs = new BoundingSphere(Cartesian3.ZERO, 1.0);
        var transform = Matrix4.fromScale(new Cartesian3(1.0, 2.0, 3.0));
        var expected = new BoundingSphere(Cartesian3.ZERO, 1.0);
        expect(BoundingSphere.transformWithoutScale(bs, transform)).toEqual(expected);
    });

    it('finds distances', function() {
        var bs = new BoundingSphere(Cartesian3.ZERO, 1.0);
        var position = new Cartesian3(-2.0, 1.0, 0.0);
        var direction = Cartesian3.UNIT_X;
        var expected = new Interval(1.0, 3.0);
        expect(BoundingSphere.computePlaneDistances(bs, position, direction)).toEqual(expected);
    });

    it('estimated distance squared to point', function() {
        var bs = new BoundingSphere(Cartesian3.ZERO, 1.0);
        var position = new Cartesian3(-2.0, 1.0, 0.0);
        var expected = Cartesian3.magnitudeSquared(position) - 1.0;
        expect(BoundingSphere.distanceSquaredTo(bs, position)).toEqual(expected);
    });

    it('projectTo2D', function() {
        var positions = getPositions();
        var projection = new GeographicProjection();

        var positions2D = [];
        for (var i = 0; i < positions.length; ++i) {
            var position = positions[i];
            var cartographic = projection.ellipsoid.cartesianToCartographic(position);
            positions2D.push(projection.project(cartographic));
        }

        var boundingSphere3D = BoundingSphere.fromPoints(positions);
        var boundingSphere2D = BoundingSphere.projectTo2D(boundingSphere3D, projection);
        var actualSphere = BoundingSphere.fromPoints(positions2D);
        actualSphere.center = new Cartesian3(actualSphere.center.z, actualSphere.center.x, actualSphere.center.y);

        expect(boundingSphere2D.center).toEqualEpsilon(actualSphere.center, CesiumMath.EPSILON6);
        expect(boundingSphere2D.radius).toBeGreaterThan(actualSphere.radius);
    });

    it('projectTo2D with result parameter', function() {
        var positions = getPositions();
        var projection = new GeographicProjection();
        var sphere = new BoundingSphere();

        var positions2D = [];
        for (var i = 0; i < positions.length; ++i) {
            var position = positions[i];
            var cartographic = projection.ellipsoid.cartesianToCartographic(position);
            positions2D.push(projection.project(cartographic));
        }

        var boundingSphere3D = BoundingSphere.fromPoints(positions);
        var boundingSphere2D = BoundingSphere.projectTo2D(boundingSphere3D, projection, sphere);
        var actualSphere = BoundingSphere.fromPoints(positions2D);
        actualSphere.center = new Cartesian3(actualSphere.center.z, actualSphere.center.x, actualSphere.center.y);

        expect(boundingSphere2D).toBe(sphere);
        expect(boundingSphere2D.center).toEqualEpsilon(actualSphere.center, CesiumMath.EPSILON6);
        expect(boundingSphere2D.radius).toBeGreaterThan(actualSphere.radius);
    });

    it('can pack and unpack', function() {
        var array = [];
        var boundingSphere = new BoundingSphere();
        boundingSphere.center = new Cartesian3(1, 2, 3);
        boundingSphere.radius = 4;
        BoundingSphere.pack(boundingSphere, array);
        expect(array.length).toEqual(BoundingSphere.packedLength);
        expect(BoundingSphere.unpack(array)).toEqual(boundingSphere);
    });

    it('can pack and unpack with offset', function() {
        var packed = new Array(3);
        var offset = 3;
        var boundingSphere = new BoundingSphere();
        boundingSphere.center = new Cartesian3(1, 2, 3);
        boundingSphere.radius = 4;

        BoundingSphere.pack(boundingSphere, packed, offset);
        expect(packed.length).toEqual(offset + BoundingSphere.packedLength);

        var result = new BoundingSphere();
        var returnedResult = BoundingSphere.unpack(packed, offset, result);
        expect(returnedResult).toBe(result);
        expect(result).toEqual(boundingSphere);
    });

    it('pack throws with undefined boundingSphere', function() {
        var array = [];
        expect(function() {
            BoundingSphere.pack(undefined, array);
        }).toThrowDeveloperError();
    });

    it('pack throws with undefined array', function() {
        var boundingSphere = new BoundingSphere();
        expect(function() {
            BoundingSphere.pack(boundingSphere, undefined);
        }).toThrowDeveloperError();
    });

    it('unpack throws with undefined array', function() {
        expect(function() {
            BoundingSphere.unpack(undefined);
        }).toThrowDeveloperError();
    });

    it('static projectTo2D throws without sphere', function() {
        expect(function() {
            BoundingSphere.projectTo2D();
        }).toThrowDeveloperError();
    });

    it('clone returns undefined with no parameter', function() {
        expect(BoundingSphere.clone()).toBeUndefined();
    });

    it('union throws with no left parameter', function() {
        var right = new BoundingSphere();
        expect(function() {
            BoundingSphere.union(undefined, right);
        }).toThrowDeveloperError();
    });

    it('union throws with no right parameter', function() {
        var left = new BoundingSphere();
        expect(function() {
            BoundingSphere.union(left, undefined);
        }).toThrowDeveloperError();
    });

    it('expand throws without a sphere', function() {
        var plane = new Cartesian3();
        expect(function() {
            BoundingSphere.expand(undefined, plane);
        }).toThrowDeveloperError();
    });

    it('expand throws without a point', function() {
        var sphere = new BoundingSphere();
        expect(function() {
            BoundingSphere.expand(sphere, undefined);
        }).toThrowDeveloperError();
    });

    it('intersectPlane throws without a sphere', function() {
        var plane = new Plane(Cartesian3.UNIT_X, 0.0);
        expect(function() {
            BoundingSphere.intersectPlane(undefined, plane);
        }).toThrowDeveloperError();
    });

    it('intersectPlane throws without a plane', function() {
        var sphere = new BoundingSphere();
        expect(function() {
            BoundingSphere.intersectPlane(sphere, undefined);
        }).toThrowDeveloperError();
    });

    it('transform throws without a sphere', function() {
        expect(function() {
            BoundingSphere.transform();
        }).toThrowDeveloperError();
    });

    it('transform throws without a transform', function() {
        var sphere = new BoundingSphere();
        expect(function() {
            BoundingSphere.transform(sphere);
        }).toThrowDeveloperError();
    });

    it('distanceSquaredTo throws without a sphere', function() {
        expect(function() {
            BoundingSphere.distanceSquaredTo();
        }).toThrowDeveloperError();
    });

    it('distanceSquaredTo throws without a cartesian', function() {
        expect(function() {
            BoundingSphere.distanceSquaredTo(new BoundingSphere());
        }).toThrowDeveloperError();
    });

    it('transformWithoutScale throws without a sphere', function() {
        expect(function() {
            BoundingSphere.transformWithoutScale();
        }).toThrowDeveloperError();
    });

    it('transformWithoutScale throws without a transform', function() {
        var sphere = new BoundingSphere();
        expect(function() {
            BoundingSphere.transformWithoutScale(sphere);
        }).toThrowDeveloperError();
    });

    it('computePlaneDistances throws without a sphere', function() {
        expect(function() {
            BoundingSphere.computePlaneDistances();
        }).toThrowDeveloperError();
    });

    it('computePlaneDistances throws without a position', function() {
        expect(function() {
            BoundingSphere.computePlaneDistances(new BoundingSphere());
        }).toThrowDeveloperError();
    });

    it('computePlaneDistances throws without a direction', function() {
        expect(function() {
            BoundingSphere.computePlaneDistances(new BoundingSphere(), new Cartesian3());
        }).toThrowDeveloperError();
    });

    it('isOccluded throws without a sphere', function() {
        expect(function() {
            BoundingSphere.isOccluded();
        }).toThrowDeveloperError();
    });

    it('isOccluded throws without an occluder', function() {
        expect(function() {
            BoundingSphere.isOccluded(new BoundingSphere());
        }).toThrowDeveloperError();
    });

    function expectBoundingSphereToContainPoint(boundingSphere, point, projection) {
        var pointInCartesian = projection.project(point);
        var distanceFromCenter = Cartesian3.magnitude(Cartesian3.subtract(pointInCartesian, boundingSphere.center, new Cartesian3()));

        // The distanceFromCenter for corner points at the height extreme should equal the
        // bounding sphere's radius.  But due to rounding errors it can end up being
        // very slightly greater.  Pull in the distanceFromCenter slightly to
        // account for this possibility.
        distanceFromCenter -= CesiumMath.EPSILON9;

        expect(distanceFromCenter).toBeLessThanOrEqualTo(boundingSphere.radius);
    }

    it('fromRectangleWithHeights2D includes specified min and max heights', function() {
        var rectangle = new Rectangle(0.1, 0.5, 0.2, 0.6);
        var projection = new GeographicProjection();
        var minHeight = -327.0;
        var maxHeight = 2456.0;
        var boundingSphere = BoundingSphere.fromRectangleWithHeights2D(rectangle, projection, minHeight, maxHeight);

        // Test that the corners are inside the bounding sphere.
        var point = Rectangle.southwest(rectangle).clone();
        point.height = minHeight;
        expectBoundingSphereToContainPoint(boundingSphere, point, projection);

        point = Rectangle.southwest(rectangle).clone();
        point.height = maxHeight;
        expectBoundingSphereToContainPoint(boundingSphere, point, projection);

        point = Rectangle.northeast(rectangle).clone();
        point.height = minHeight;
        expectBoundingSphereToContainPoint(boundingSphere, point, projection);

        point = Rectangle.northeast(rectangle).clone();
        point.height = maxHeight;
        expectBoundingSphereToContainPoint(boundingSphere, point, projection);

        point = Rectangle.southeast(rectangle).clone();
        point.height = minHeight;
        expectBoundingSphereToContainPoint(boundingSphere, point, projection);

        point = Rectangle.southeast(rectangle).clone();
        point.height = maxHeight;
        expectBoundingSphereToContainPoint(boundingSphere, point, projection);

        point = Rectangle.northwest(rectangle).clone();
        point.height = minHeight;
        expectBoundingSphereToContainPoint(boundingSphere, point, projection);

        point = Rectangle.northwest(rectangle).clone();
        point.height = maxHeight;
        expectBoundingSphereToContainPoint(boundingSphere, point, projection);

        // Test that the center is inside the bounding sphere
        point = Rectangle.center(rectangle).clone();
        point.height = minHeight;
        expectBoundingSphereToContainPoint(boundingSphere, point, projection);

        point = Rectangle.center(rectangle).clone();
        point.height = maxHeight;
        expectBoundingSphereToContainPoint(boundingSphere, point, projection);

        // Test that the edge midpoints are inside the bounding sphere.
        point = new Cartographic(Rectangle.center(rectangle).longitude, rectangle.south, minHeight);
        expectBoundingSphereToContainPoint(boundingSphere, point, projection);

        point = new Cartographic(Rectangle.center(rectangle).longitude, rectangle.south, maxHeight);
        expectBoundingSphereToContainPoint(boundingSphere, point, projection);

        point = new Cartographic(Rectangle.center(rectangle).longitude, rectangle.north, minHeight);
        expectBoundingSphereToContainPoint(boundingSphere, point, projection);

        point = new Cartographic(Rectangle.center(rectangle).longitude, rectangle.north, maxHeight);
        expectBoundingSphereToContainPoint(boundingSphere, point, projection);

        point = new Cartographic(rectangle.west, Rectangle.center(rectangle).latitude, minHeight);
        expectBoundingSphereToContainPoint(boundingSphere, point, projection);

        point = new Cartographic(rectangle.west, Rectangle.center(rectangle).latitude, maxHeight);
        expectBoundingSphereToContainPoint(boundingSphere, point, projection);

        point = new Cartographic(rectangle.east, Rectangle.center(rectangle).latitude, minHeight);
        expectBoundingSphereToContainPoint(boundingSphere, point, projection);

        point = new Cartographic(rectangle.east, Rectangle.center(rectangle).latitude, maxHeight);
        expectBoundingSphereToContainPoint(boundingSphere, point, projection);
    });

    it('computes the volume of a BoundingSphere', function() {
        var sphere = new BoundingSphere(new Cartesian3(), 1.0);
        var computedVolume = sphere.volume();
        var expectedVolume = (4.0 / 3.0) * CesiumMath.PI;
        expect(computedVolume).toEqualEpsilon(expectedVolume, CesiumMath.EPSILON6);
    });

    createPackableSpecs(BoundingSphere, new BoundingSphere(new Cartesian3(1.0, 2.0, 3.0), 4.0), [1.0, 2.0, 3.0, 4.0]);
});
